package com.cm.kits;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

/**
 * 哈希工具
 *
 * @author LWang 2023.01.19
 * @since 1.0.0
 */
public final class HashKits {
    private HashKits() {
    }

    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();

    /**
     * 将字节数组转换为 Hex
     *
     * @param bytes 要转换的字节数据
     * @return 转换后的 HEX 字符串
     */
    public static String toHex(byte[] bytes) {
        return toHex(bytes, false);
    }

    /**
     * 将字节数组转换为 Hex
     *
     * @param bytes     要转换的字节数据
     * @param upperCase 结果转换为大写字母
     * @return 转换后的 HEX 字符串
     */
    public static String toHex(byte[] bytes, boolean upperCase) {
        if (ArrayUtils.isEmpty(bytes)) {
            return StringUtils.EMPTY;
        }
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(HEX_DIGITS[0x0F & (b >> 4)]);
            result.append(HEX_DIGITS[0x0F & b]);
        }
        return upperCase ? result.toString().toUpperCase() : result.toString().toLowerCase();
    }

    /**
     * 将 Hex 字符串反编码为 byte 数组
     *
     * @param hexStr 要反编码的字符串
     * @return 反编码结果
     */
    public static byte[] decodeHex(String hexStr) {
        if (StringUtils.isBlank(hexStr)) {
            throw new IllegalArgumentException("反编码 HEX 信息错误，目标字符串为空");
        }
        return decodeHex(hexStr.toCharArray());
    }

    /**
     * 将 Hex 字符数组反编码为 byte 数组
     *
     * @param chars 要反编码的字符数组
     * @return 反编码结果
     */
    public static byte[] decodeHex(char[] chars) {
        if (ArrayUtils.isEmpty(chars)) {
            throw new IllegalArgumentException("反编码 HEX 信息错误，目标字符数组为空！");
        }
        if (chars.length % 2 != 0) {
            throw new IllegalArgumentException("反编码 HEX 信息错误，目标字符数组长度不是偶数！");
        }
        ByteBuffer byteBuffer = ByteBuffer.allocate(chars.length / 2);
        for (int i = 0; i < chars.length; i += 2) {
            byteBuffer.put((byte) ((toDigit(chars[i]) << 4) | toDigit(chars[i + 1])));
        }
        return byteBuffer.array();
    }

    private static int toDigit(char c) {
        int digit = Character.digit(c, 16);
        if (digit < 0) {
            throw new IllegalArgumentException(String.format("无法将字符 %c 转换为十六进制数字！", c));
        }
        return digit;
    }

    /**
     * 将字节数组转换为 MD5 摘要
     *
     * @param bytes 要转换的字节数组
     * @return 转换结果，HEX 字符串
     */
    public static String md5(byte[] bytes) {
        return md5(bytes, false);
    }

    /**
     * 将字节数组转换为 MD5 摘要
     *
     * @param bytes     要转换的字节数组
     * @param upperCase 结果是否大写
     * @return 转换结果，HEX 字符串
     */
    public static String md5(byte[] bytes, boolean upperCase) {
        return hash("MD5", bytes, upperCase);
    }

    /**
     * 将字节数组转换为 SHA1 摘要
     *
     * @param bytes 要转换的字节数组
     * @return 转换结果，HEX 字符串
     */
    public static String sha1(byte[] bytes) {
        return sha1(bytes, false);
    }

    /**
     * 将字节数组转换为 SHA1 摘要
     *
     * @param bytes     要转换的字节数组
     * @param upperCase 结果是否大写
     * @return 转换结果，HEX 字符串
     */
    public static String sha1(byte[] bytes, boolean upperCase) {
        return hash("SHA-1", bytes, upperCase);
    }

    /**
     * 将字节数组转换为 SHA256 摘要
     *
     * @param bytes 要转换的字节数组
     * @return 转换结果，HEX 字符串
     */
    public static String sha256(byte[] bytes) {
        return sha256(bytes, false);
    }

    /**
     * 将字节数组转换为 SHA256 摘要
     *
     * @param bytes     要转换的字节数组
     * @param upperCase 结果是否大写
     * @return 转换结果，HEX 字符串
     */
    public static String sha256(byte[] bytes, boolean upperCase) {
        return hash("SHA-256", bytes, upperCase);
    }

    /**
     * 将字节数组转换为 SHA512 摘要
     *
     * @param bytes 要转换的字节数组
     * @return 转换结果，HEX 字符串
     */
    public static String sha512(byte[] bytes) {
        return sha512(bytes, false);
    }

    /**
     * 将字节数组转换为 SHA512 摘要
     *
     * @param bytes     要转换的字节数组
     * @param upperCase 结果是否大写
     * @return 转换结果，HEX 字符串
     */
    public static String sha512(byte[] bytes, boolean upperCase) {
        return hash("SHA-512", bytes, upperCase);
    }

    /**
     * 生成基于 SHA 256 摘要算法的签名数据，结果使用 HEX 方式编码
     *
     * @param parameters  要进行签名的数据
     * @param secretKey   安全密钥键，如果为空，表示不设置安全密钥
     * @param secretValue 安全密钥值，如果 secretKey 不为空，则 secretValue 必须存在
     * @return 签名结果
     */
    public static String signatureWithSha256(final Map<String, String> parameters, String secretKey, String secretValue) {
        if (Objects.isNull(parameters)) {
            throw new IllegalArgumentException("签名数据不能为空！");
        }
        if (StringUtils.isNotBlank(secretKey) && StringUtils.isBlank(secretValue)) {
            throw new IllegalArgumentException("安全密钥值不能为空！");
        }
        var sortMap = new TreeMap<>(parameters);
        if (StringUtils.isNotBlank(secretKey)) {
            sortMap.put(secretKey, secretValue);
        }
        var data = StringUtils.join(sortMap.values()).getBytes(StandardCharsets.UTF_8);
        return sha256(data);
    }

    /**
     * 对基于 SHA 256 摘要算法生成的签名进行验签
     *
     * @param parameters  待验签的数据
     * @param signature   签名数据
     * @param secretKey   安全密钥键，如果为空，表示不设置安全密钥
     * @param secretValue 安全密钥值，如果 secretKey 不为空，则 secretValue 必须存在
     * @return 验签结果
     */
    public static boolean verifyWithSha256(final Map<String, String> parameters, String signature, String secretKey, String secretValue) {
        return Objects.equals(
                signatureWithSha256(parameters, secretKey, secretValue),
                signature
        );
    }

    /**
     * 进行摘要计算
     *
     * @param algorithm 算法
     * @param bytes     要进行摘要计算的字节数组
     * @param upperCase 结果是否大写
     * @return 转换结果，HEX 字符串
     */
    private static String hash(String algorithm, byte[] bytes, boolean upperCase) {
        try {
            MessageDigest md = MessageDigest.getInstance(algorithm);
            return toHex(md.digest(bytes), upperCase);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
